WaveFileCreator   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 180
dl 0
loc 422
rs 9.36
c 0
b 0
f 0
wmc 38

21 Functions

Rating   Name   Duplication   Size   Complexity  
A toBuffer 0 4 1
A fromBuffer 0 5 1
A constructor 0 31 1
A createALawMulawHeader_ 0 13 1
A createADPCMHeader_ 0 16 1
A validateWavHeader_ 0 10 3
A newWavFile_ 0 19 2
A createPCMHeader_ 0 20 1
A createExtensibleHeader_ 0 13 1
A updateDataType_ 0 12 2
A validateBitDepth_ 0 10 3
A bitDepthFromFmt_ 0 11 4
A makeWavHeader_ 0 19 4
A setiXML 0 7 2
A set_PMX 0 7 2
A getSample 0 9 2
A getiXML 0 3 1
A get_PMX 0 3 1
A getSamples 0 16 2
A setSample 0 7 2
A fromScratch 0 6 1
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileCreator class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { WaveFileParser } from './wavefile-parser';
31
import { interleave, deInterleave } from './parsers/interleave';
32
import { validateNumChannels } from './validators/validate-num-channels'; 
33
import { validateSampleRate } from './validators/validate-sample-rate';
34
import { packArrayTo, unpackArrayTo, packTo, unpack } from './parsers/binary';
35
36
/**
37
 * A class to read, write and create wav files.
38
 * @extends WaveFileParser
39
 * @ignore
40
 */
41
export class WaveFileCreator extends WaveFileParser {
42
43
  constructor() {
44
    super();
45
    /**
46
     * The bit depth code according to the samples.
47
     * @type {string}
48
     */
49
    this.bitDepth = '0';
50
    /**
51
     * @type {!{bits: number, be: boolean}}
52
     * @protected
53
     */
54
    this.dataType = {bits: 0, be: false};
55
    /**
56
     * Audio formats.
57
     * Formats not listed here should be set to 65534,
58
     * the code for WAVE_FORMAT_EXTENSIBLE
59
     * @enum {number}
60
     * @protected
61
     */
62
    this.WAV_AUDIO_FORMATS = {
63
      '4': 17,
64
      '8': 1,
65
      '8a': 6,
66
      '8m': 7,
67
      '16': 1,
68
      '24': 1,
69
      '32': 1,
70
      '32f': 3,
71
      '64': 3
72
    };
73
  }
74
75
  /**
76
   * Set up the WaveFileCreator object based on the arguments passed.
77
   * Existing chunks are reset.
78
   * @param {number} numChannels The number of channels.
79
   * @param {number} sampleRate The sample rate.
80
   *    Integers like 8000, 44100, 48000, 96000, 192000.
81
   * @param {string} bitDepthCode The audio bit depth code.
82
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
83
   *    or any value between '8' and '32' (like '12').
84
   * @param {!(Array|TypedArray)} samples The samples.
85
   * @param {Object=} options Optional. Used to force the container
86
   *    as RIFX with {'container': 'RIFX'}
87
   * @throws {Error} If any argument does not meet the criteria.
88
   */
89
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options) {
90
    options = options || {};
91
    // reset all chunks
92
    this.clearHeaders();
93
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
94
  }
95
96
  /**
97
   * Set up the WaveFileParser object from a byte buffer.
98
   * @param {!Uint8Array} wavBuffer The buffer.
99
   * @param {boolean=} [samples=true] True if the samples should be loaded.
100
   * @throws {Error} If container is not RIFF, RIFX or RF64.
101
   * @throws {Error} If format is not WAVE.
102
   * @throws {Error} If no 'fmt ' chunk is found.
103
   * @throws {Error} If no 'data' chunk is found.
104
   */
105
  fromBuffer(wavBuffer, samples=true) {
106
    super.fromBuffer(wavBuffer, samples);
107
    this.bitDepthFromFmt_();
108
    this.updateDataType_();
109
  }
110
111
  /**
112
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
113
   * The return value of this method can be written straight to disk.
114
   * @return {!Uint8Array} A wav file.
115
   * @throws {Error} If bit depth is invalid.
116
   * @throws {Error} If the number of channels is invalid.
117
   * @throws {Error} If the sample rate is invalid.
118
   */
119
  toBuffer() {
120
    this.validateWavHeader_();
121
    return super.toBuffer();
122
  }
123
124
  /**
125
   * Return the samples packed in a Float64Array.
126
   * @param {boolean=} [interleaved=false] True to return interleaved samples,
127
   *   false to return the samples de-interleaved.
128
   * @param {Function=} [OutputObject=Float64Array] The sample container.
129
   * @return {!(Array|TypedArray)} the samples.
130
   */
131
  getSamples(interleaved=false, OutputObject=Float64Array) {
132
    /**
133
     * A Float64Array created with a size to match the
134
     * the length of the samples.
135
     * @type {!(Array|TypedArray)}
136
     */
137
    let samples = new OutputObject(
138
      this.data.samples.length / (this.dataType.bits / 8));
139
    // Unpack all the samples
140
    unpackArrayTo(this.data.samples, this.dataType, samples,
141
      0, this.data.samples.length);
142
    if (!interleaved && this.fmt.numChannels > 1) {
143
      return deInterleave(samples, this.fmt.numChannels, OutputObject);
144
    }
145
    return samples;
146
  }
147
148
  /**
149
   * Return the sample at a given index.
150
   * @param {number} index The sample index.
151
   * @return {number} The sample.
152
   * @throws {Error} If the sample index is off range.
153
   */
154
  getSample(index) {
155
    index = index * (this.dataType.bits / 8);
156
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
157
      throw new Error('Range error');
158
    }
159
    return unpack(
160
      this.data.samples.slice(index, index + this.dataType.bits / 8),
161
      this.dataType);
162
  }
163
164
  /**
165
   * Set the sample at a given index.
166
   * @param {number} index The sample index.
167
   * @param {number} sample The sample.
168
   * @throws {Error} If the sample index is off range.
169
   */
170
  setSample(index, sample) {
171
    index = index * (this.dataType.bits / 8);
172
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
173
      throw new Error('Range error');
174
    }
175
    packTo(sample, this.dataType, this.data.samples, index, true);
176
  }
177
178
  /**
179
   * Return the value of the iXML chunk.
180
   * @return {string} The contents of the iXML chunk.
181
   */
182
  getiXML() {
183
    return this.iXML.value;
184
  }
185
186
  /**
187
   * Set the value of the iXML chunk.
188
   * @param {string} iXMLValue The value for the iXML chunk.
189
   * @throws {TypeError} If the value is not a string.
190
   */
191
  setiXML(iXMLValue) {
192
    if (typeof iXMLValue !== 'string') {
193
      throw new TypeError('iXML value must be a string.');
194
    }
195
    this.iXML.value = iXMLValue;
196
    this.iXML.chunkId = 'iXML';
197
  }
198
199
  /**
200
   * Get the value of the _PMX chunk.
201
   * @return {string} The contents of the _PMX chunk.
202
   */
203
  get_PMX() {
204
    return this._PMX.value;
205
  }
206
207
  /**
208
   * Set the value of the _PMX chunk.
209
   * @param {string} _PMXValue The value for the _PMX chunk.
210
   * @throws {TypeError} If the value is not a string.
211
   */
212
  set_PMX(_PMXValue) {
213
    if (typeof _PMXValue !== 'string') {
214
      throw new TypeError('_PMX value must be a string.');
215
    }
216
    this._PMX.value = _PMXValue;
217
    this._PMX.chunkId = '_PMX';
218
  }
219
220
  /**
221
   * Set up the WaveFileCreator object based on the arguments passed.
222
   * @param {number} numChannels The number of channels.
223
   * @param {number} sampleRate The sample rate.
224
   *   Integers like 8000, 44100, 48000, 96000, 192000.
225
   * @param {string} bitDepthCode The audio bit depth code.
226
   *   One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
227
   *   or any value between '8' and '32' (like '12').
228
   * @param {!(Array|TypedArray)} samples The samples.
229
   * @param {Object} options Used to define the container.
230
   * @throws {Error} If any argument does not meet the criteria.
231
   * @private
232
   */
233
  newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options) {
234
    if (!options.container) {
235
      options.container = 'RIFF';
236
    }
237
    this.container = options.container;
238
    this.bitDepth = bitDepthCode;
239
    samples = interleave(samples);
240
    this.updateDataType_();
241
    /** @type {number} */
242
    let numBytes = this.dataType.bits / 8;
243
    this.data.samples = new Uint8Array(samples.length * numBytes);
244
    packArrayTo(samples, this.dataType, this.data.samples, 0, true);
245
    this.makeWavHeader_(
246
      bitDepthCode, numChannels, sampleRate,
247
      numBytes, this.data.samples.length, options);
248
    this.data.chunkId = 'data';
249
    this.data.chunkSize = this.data.samples.length;
250
    this.validateWavHeader_();
251
  }
252
253
  /**
254
   * Define the header of a wav file.
255
   * @param {string} bitDepthCode The audio bit depth
256
   * @param {number} numChannels The number of channels
257
   * @param {number} sampleRate The sample rate.
258
   * @param {number} numBytes The number of bytes each sample use.
259
   * @param {number} samplesLength The length of the samples in bytes.
260
   * @param {!Object} options The extra options, like container defintion.
261
   * @private
262
   */
263
  makeWavHeader_(
264
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
265
    if (bitDepthCode == '4') {
266
      this.createADPCMHeader_(
267
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
268
269
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
270
      this.createALawMulawHeader_(
271
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
272
273
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
274
        numChannels > 2) {
275
      this.createExtensibleHeader_(
276
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
277
278
    } else {
279
      this.createPCMHeader_(
280
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
281
    }
282
  }
283
284
  /**
285
   * Create the header of a linear PCM wave file.
286
   * @param {string} bitDepthCode The audio bit depth
287
   * @param {number} numChannels The number of channels
288
   * @param {number} sampleRate The sample rate.
289
   * @param {number} numBytes The number of bytes each sample use.
290
   * @param {number} samplesLength The length of the samples in bytes.
291
   * @param {!Object} options The extra options, like container defintion.
292
   * @private
293
   */
294
  createPCMHeader_(
295
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
296
    this.container = options.container;
297
    this.chunkSize = 36 + samplesLength;
298
    this.format = 'WAVE';
299
    this.bitDepth = bitDepthCode;
300
    this.fmt = {
301
      chunkId: 'fmt ',
302
      chunkSize: 16,
303
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
304
      numChannels: numChannels,
305
      sampleRate: sampleRate,
306
      byteRate: (numChannels * numBytes) * sampleRate,
307
      blockAlign: numChannels * numBytes,
308
      bitsPerSample: parseInt(bitDepthCode, 10),
309
      cbSize: 0,
310
      validBitsPerSample: 0,
311
      dwChannelMask: 0,
312
      subformat: []
313
    };
314
  }
315
316
  /**
317
   * Create the header of a ADPCM wave file.
318
   * @param {string} bitDepthCode The audio bit depth
319
   * @param {number} numChannels The number of channels
320
   * @param {number} sampleRate The sample rate.
321
   * @param {number} numBytes The number of bytes each sample use.
322
   * @param {number} samplesLength The length of the samples in bytes.
323
   * @param {!Object} options The extra options, like container defintion.
324
   * @private
325
   */
326
  createADPCMHeader_(
327
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
328
    this.createPCMHeader_(
329
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
330
    this.chunkSize = 40 + samplesLength;
331
    this.fmt.chunkSize = 20;
332
    this.fmt.byteRate = 4055;
333
    this.fmt.blockAlign = 256;
334
    this.fmt.bitsPerSample = 4;
335
    this.fmt.cbSize = 2;
336
    this.fmt.validBitsPerSample = 505;
337
    this.fact = {
338
      chunkId: 'fact',
339
      chunkSize: 4,
340
      dwSampleLength: samplesLength * 2
341
    };
342
  }
343
344
  /**
345
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
346
   * @param {string} bitDepthCode The audio bit depth
347
   * @param {number} numChannels The number of channels
348
   * @param {number} sampleRate The sample rate.
349
   * @param {number} numBytes The number of bytes each sample use.
350
   * @param {number} samplesLength The length of the samples in bytes.
351
   * @param {!Object} options The extra options, like container defintion.
352
   * @private
353
   */
354
  createExtensibleHeader_(
355
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
356
    this.createPCMHeader_(
357
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
358
    this.chunkSize = 36 + 24 + samplesLength;
359
    this.fmt.chunkSize = 40;
360
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
361
    this.fmt.cbSize = 22;
362
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
363
    this.fmt.dwChannelMask = dwChannelMask_(numChannels);
364
    // subformat 128-bit GUID as 4 32-bit values
365
    // only supports uncompressed integer PCM samples
366
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
367
  }
368
369
  /**
370
   * Create the header of mu-Law and A-Law wave files.
371
   * @param {string} bitDepthCode The audio bit depth
372
   * @param {number} numChannels The number of channels
373
   * @param {number} sampleRate The sample rate.
374
   * @param {number} numBytes The number of bytes each sample use.
375
   * @param {number} samplesLength The length of the samples in bytes.
376
   * @param {!Object} options The extra options, like container defintion.
377
   * @private
378
   */
379
  createALawMulawHeader_(
380
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
381
    this.createPCMHeader_(
382
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
383
    this.chunkSize = 40 + samplesLength;
384
    this.fmt.chunkSize = 20;
385
    this.fmt.cbSize = 2;
386
    this.fmt.validBitsPerSample = 8;
387
    this.fact = {
388
      chunkId: 'fact',
389
      chunkSize: 4,
390
      dwSampleLength: samplesLength
391
    };
392
  }
393
394
  /**
395
   * Set the string code of the bit depth based on the 'fmt ' chunk.
396
   * @private
397
   */
398
  bitDepthFromFmt_() {
399
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
400
      this.bitDepth = '32f';
401
    } else if (this.fmt.audioFormat === 6) {
402
      this.bitDepth = '8a';
403
    } else if (this.fmt.audioFormat === 7) {
404
      this.bitDepth = '8m';
405
    } else {
406
      this.bitDepth = this.fmt.bitsPerSample.toString();
407
    }
408
  }
409
410
  /**
411
   * Validate the bit depth.
412
   * @return {boolean} True is the bit depth is valid.
413
   * @throws {Error} If bit depth is invalid.
414
   * @private
415
   */
416
  validateBitDepth_() {
417
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
418
      if (parseInt(this.bitDepth, 10) > 8 &&
419
          parseInt(this.bitDepth, 10) < 54) {
420
        return true;
421
      }
422
      throw new Error('Invalid bit depth.');
423
    }
424
    return true;
425
  }
426
427
  /**
428
   * Update the type definition used to read and write the samples.
429
   * @private
430
   */
431
  updateDataType_() {
432
    this.dataType = {
433
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
434
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
435
      signed: this.bitDepth != '8',
436
      be: this.container == 'RIFX'
437
    };
438
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
439
      this.dataType.bits = 8;
440
      this.dataType.signed = false;
441
    }
442
  }
443
444
  /**
445
   * Validate the header of the file.
446
   * @throws {Error} If bit depth is invalid.
447
   * @throws {Error} If the number of channels is invalid.
448
   * @throws {Error} If the sample rate is invalid.
449
   * @ignore
450
   * @private
451
   */
452
  validateWavHeader_() {
453
    this.validateBitDepth_();
454
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
455
      throw new Error('Invalid number of channels.');
456
    }
457
    if (!validateSampleRate(
458
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
459
      throw new Error('Invalid sample rate.');
460
    }
461
  }
462
}
463
464
/**
465
 * Return the value for dwChannelMask according to the number of channels.
466
 * @param {number} numChannels the number of channels.
467
 * @return {number} the dwChannelMask value.
468
 * @private
469
 */
470
function dwChannelMask_(numChannels) {
471
  /** @type {number} */
472
  let mask = 0;
473
  // mono = FC
474
  if (numChannels === 1) {
475
    mask = 0x4;
476
  // stereo = FL, FR
477
  } else if (numChannels === 2) {
478
    mask = 0x3;
479
  // quad = FL, FR, BL, BR
480
  } else if (numChannels === 4) {
481
    mask = 0x33;
482
  // 5.1 = FL, FR, FC, LF, BL, BR
483
  } else if (numChannels === 6) {
484
    mask = 0x3F;
485
  // 7.1 = FL, FR, FC, LF, BL, BR, SL, SR
486
  } else if (numChannels === 8) {
487
    mask = 0x63F;
488
  }
489
  return mask;
490
}
491